import { GlEmptyState, GlBadge, GlPopover } from '@gitlab/ui';
import { nextTick } from 'vue';
import AiGenieLoader from 'ee/ai/components/ai_genie_loader.vue';
import AiGenieChat from 'ee/ai/components/ai_genie_chat.vue';
import AiGenieChatConversation from 'ee/ai/components/ai_genie_chat_conversation.vue';
import AiPredefinedPrompts from 'ee/ai/components/ai_predefined_prompts.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { i18n, GENIE_CHAT_MODEL_ROLES, GENIE_CHAT_RESET_MESSAGE } from 'ee/ai/constants';

describe('AiGenieChat', () => {
  let wrapper;

  const createComponent = ({
    propsData = {},
    data = {},
    scopedSlots = {},
    slots = {},
    glFeatures = {},
  } = {}) => {
    jest.spyOn(AiGenieLoader.methods, 'computeTransitionWidth').mockImplementation();

    wrapper = shallowMountExtended(AiGenieChat, {
      propsData,
      data() {
        return {
          ...data,
        };
      },
      scopedSlots,
      slots,
      stubs: {
        AiGenieLoader,
      },
      provide: {
        glFeatures,
      },
    });
  };

  const findChatComponent = () => wrapper.findByTestId('chat-component');
  const findCloseButton = () => wrapper.findByTestId('chat-close-button');
  const findChatConversations = () => wrapper.findAllComponents(AiGenieChatConversation);
  const findCustomLoader = () => wrapper.findComponent(AiGenieLoader);
  const findError = () => wrapper.findByTestId('chat-error');
  const findFooter = () => wrapper.findByTestId('chat-footer');
  const findPromptForm = () => wrapper.findByTestId('chat-prompt-form');
  const findGeneratedByAI = () => wrapper.findByText(i18n.GENIE_CHAT_LEGAL_GENERATED_BY_AI);
  const findBadge = () => wrapper.findComponent(GlBadge);
  const findPopover = () => wrapper.findComponent(GlPopover);
  const findEmptyState = () => wrapper.findComponent(GlEmptyState);
  const findPredefined = () => wrapper.findComponent(AiPredefinedPrompts);
  const findChatInput = () => wrapper.findByTestId('chat-prompt-input');
  const findCloseChatButton = () => wrapper.findByTestId('chat-close-button');
  const findLegalDisclaimer = () => wrapper.findByTestId('chat-legal-disclaimer');

  beforeEach(() => {
    createComponent();
  });

  const promptStr = 'foo';
  const messages = [
    {
      role: GENIE_CHAT_MODEL_ROLES.user,
      content: promptStr,
    },
  ];

  describe('rendering', () => {
    it.each`
      desc                                  | component            | shouldRender
      ${'renders root component'}           | ${findChatComponent} | ${true}
      ${'renders experimental label'}       | ${findBadge}         | ${true}
      ${'renders empty state'}              | ${findEmptyState}    | ${true}
      ${'renders predefined prompts'}       | ${findPredefined}    | ${true}
      ${'does not render loading skeleton'} | ${findCustomLoader}  | ${false}
      ${'does not render chat error'}       | ${findError}         | ${false}
      ${'does not render chat input'}       | ${findChatInput}     | ${false}
      ${'renders a generated by AI note'}   | ${findGeneratedByAI} | ${true}
    `('$desc', ({ component, shouldRender }) => {
      expect(component().exists()).toBe(shouldRender);
    });

    describe('when messages exist', () => {
      it('scrolls to the bottom on load', async () => {
        createComponent({ propsData: { messages } });
        const { element } = findChatComponent();
        jest.spyOn(element, 'scrollHeight', 'get').mockReturnValue(200);

        await nextTick();

        expect(element.scrollTop).toEqual(200);
      });
    });

    describe('conversations', () => {
      it('renders one conversation when no reset message is present', () => {
        const newMessages = [
          {
            role: GENIE_CHAT_MODEL_ROLES.user,
            content: 'How are you?',
          },
          {
            role: GENIE_CHAT_MODEL_ROLES.assistant,
            content: 'Great!',
          },
        ];
        createComponent({ propsData: { messages: newMessages } });

        expect(findChatConversations().length).toEqual(1);
        expect(findChatConversations().at(0).props('showDelimiter')).toEqual(false);
      });

      it('renders one conversation when no message is present', () => {
        const newMessages = [];
        createComponent({ propsData: { messages: newMessages } });

        expect(findChatConversations().length).toEqual(0);
      });

      it('splits it up into multiple conversations when reset message is present', () => {
        const newMessages = [
          {
            role: GENIE_CHAT_MODEL_ROLES.user,
            content: 'Message 1',
          },
          {
            role: GENIE_CHAT_MODEL_ROLES.assistant,
            content: 'Great!',
          },
          {
            role: GENIE_CHAT_MODEL_ROLES.user,
            content: GENIE_CHAT_RESET_MESSAGE,
          },
        ];
        createComponent({ propsData: { messages: newMessages } });

        expect(findChatConversations().length).toEqual(2);
        expect(findChatConversations().at(0).props('showDelimiter')).toEqual(false);
        expect(findChatConversations().at(1).props('showDelimiter')).toEqual(true);
      });
    });

    describe('slots', () => {
      const slotContent = 'As Gregor Samsa awoke one morning from uneasy dreams';

      it.each`
        desc                 | slot           | content        | isChatAvailable | shouldRenderSlotContent
        ${'renders'}         | ${'hero'}      | ${slotContent} | ${true}         | ${true}
        ${'renders'}         | ${'hero'}      | ${slotContent} | ${false}        | ${true}
        ${'does not render'} | ${'subheader'} | ${slotContent} | ${false}        | ${true}
        ${'renders'}         | ${'subheader'} | ${slotContent} | ${true}         | ${true}
      `(
        '$desc the $content passed to the $slot slot when isChatAvailable is $isChatAvailable',
        ({ slot, content, isChatAvailable, shouldRenderSlotContent }) => {
          createComponent({
            propsData: { isChatAvailable },
            slots: { [slot]: content },
          });
          if (shouldRenderSlotContent) {
            expect(wrapper.text()).toContain(content);
          } else {
            expect(wrapper.text()).not.toContain(content);
          }
        },
      );
    });

    it('sets correct props on the Experiment badge', () => {
      const badgeType = 'neutral';
      const badgeSize = 'md';
      expect(findBadge().props('variant')).toBe(badgeType);
      expect(findBadge().props('size')).toBe(badgeSize);
      expect(findBadge().text()).toBe(i18n.EXPERIMENT_BADGE);
    });

    it('shows the popover when the Experiment badge is clicked', () => {
      createComponent();
      expect(findPopover().props('target')).toBe(findBadge().vm.$el.id);
    });
  });

  describe('chat', () => {
    it('does not render prompt input by default', () => {
      createComponent({ propsData: { messages } });
      expect(findChatInput().exists()).toBe(false);
    });

    it('renders prompt input if `isChatAvailable` prop is `true`', () => {
      createComponent({ propsData: { messages, isChatAvailable: true } });
      expect(findChatInput().exists()).toBe(true);
    });

    it('renders the legal disclaimer if `isChatAvailable` prop is `true', () => {
      createComponent({ propsData: { messages, isChatAvailable: true } });
      expect(findLegalDisclaimer().exists()).toBe(true);
    });

    describe('reset', () => {
      const clickSubmit = () =>
        findPromptForm().vm.$emit('submit', {
          preventDefault: jest.fn(),
          stopPropagation: jest.fn(),
        });

      it('emits the event with the reset prompt', () => {
        createComponent({
          propsData: { messages, isChatAvailable: true },
          data: { prompt: GENIE_CHAT_RESET_MESSAGE },
        });
        clickSubmit();

        expect(wrapper.emitted('send-chat-prompt')).toEqual([[GENIE_CHAT_RESET_MESSAGE]]);
        expect(findChatConversations().length).toEqual(1);
      });

      it('reset does nothing when chat is loading', () => {
        createComponent({
          propsData: { messages, isChatAvailable: true, isLoading: true },
          data: { prompt: GENIE_CHAT_RESET_MESSAGE },
        });
        clickSubmit();

        expect(wrapper.emitted('send-chat-prompt')).toBeUndefined();
        expect(findChatConversations().length).toEqual(1);
      });

      it('reset does nothing when there are no messages', () => {
        createComponent({
          propsData: { messages: [], isChatAvailable: true },
          data: { prompt: GENIE_CHAT_RESET_MESSAGE },
        });
        clickSubmit();

        expect(wrapper.emitted('send-chat-prompt')).toBeUndefined();
        expect(findChatConversations().length).toEqual(0);
      });

      it('reset does nothing when last message was a reset message', () => {
        const existingMessages = [
          ...messages,
          {
            role: GENIE_CHAT_MODEL_ROLES.user,
            content: GENIE_CHAT_RESET_MESSAGE,
          },
        ];
        createComponent({
          propsData: {
            isLoading: false,
            messages: existingMessages,
            isChatAvailable: true,
          },
          data: { prompt: GENIE_CHAT_RESET_MESSAGE },
        });
        clickSubmit();

        expect(wrapper.emitted('send-chat-prompt')).toBeUndefined();

        expect(findChatConversations().length).toEqual(2);
        expect(findChatConversations().at(0).props('messages')).toEqual(messages);
        expect(findChatConversations().at(1).props('messages')).toEqual([]);
      });
    });
  });

  describe('interaction', () => {
    it('is hidden after the header button is clicked', async () => {
      findCloseButton().vm.$emit('click');
      await nextTick();
      expect(findChatComponent().exists()).toBe(false);
    });

    it('resets the hidden status of the component on loading', async () => {
      createComponent({ data: { isHidden: true } });
      expect(findChatComponent().exists()).toBe(false);
      // setProps is justified here because we are testing the component's
      // reactive behavior which consistutes an exception
      // See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
      wrapper.setProps({
        isLoading: true,
      });
      await nextTick();
      expect(findChatComponent().exists()).toBe(true);
    });

    it('resets the prompt when new messages are added', async () => {
      const prompt = 'foo';
      createComponent({ propsData: { isChatAvailable: true }, data: { prompt } });
      expect(findChatInput().props('value')).toBe(prompt);
      // setProps is justified here because we are testing the component's
      // reactive behavior which consistutes an exception
      // See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
      wrapper.setProps({
        messages,
      });
      await waitForPromises();
      expect(findChatInput().props('value')).toBe('');
    });

    it('renders custom loader when isLoading', () => {
      createComponent({ propsData: { isLoading: true } });
      expect(findCustomLoader().exists()).toBe(true);
    });

    it('renders alert if error', () => {
      const errorMessage = 'Something went Wrong';
      createComponent({ propsData: { error: errorMessage } });
      expect(findError().text()).toBe(errorMessage);
    });

    it('hides the chat on button click and emits an event', () => {
      createComponent({ propsData: { messages } });
      expect(wrapper.vm.$data.isHidden).toBe(false);
      findCloseChatButton().vm.$emit('click');
      expect(wrapper.vm.$data.isHidden).toBe(true);
      expect(wrapper.emitted('chat-hidden')).toBeDefined();
    });

    it('does not render the empty state when there are messages available', () => {
      createComponent({ propsData: { messages } });
      expect(findEmptyState().exists()).toBe(false);
    });

    describe('scrolling', () => {
      let element;

      beforeEach(() => {
        createComponent({ propsData: { messages, isChatAvailable: true } });
        element = findChatComponent().element;
      });

      it('when scrolling to the bottom it removes the scrim class', async () => {
        jest.spyOn(element, 'scrollTop', 'get').mockReturnValue(100);
        jest.spyOn(element, 'offsetHeight', 'get').mockReturnValue(100);
        jest.spyOn(element, 'scrollHeight', 'get').mockReturnValue(200);

        findChatComponent().trigger('scroll');

        await nextTick();

        expect(findFooter().classes()).not.toContain('gl-drawer-body-scrim-on-footer');
      });

      it('when scrolling up it adds the scrim class', async () => {
        jest.spyOn(element, 'scrollTop', 'get').mockReturnValue(50);
        jest.spyOn(element, 'offsetHeight', 'get').mockReturnValue(100);
        jest.spyOn(element, 'scrollHeight', 'get').mockReturnValue(200);

        findChatComponent().trigger('scroll');

        await nextTick();

        expect(findFooter().classes()).toContain('gl-drawer-body-scrim-on-footer');
      });
    });

    describe('predefined prompts', () => {
      const prompts = ['what is a fork'];

      beforeEach(() => {
        createComponent({ propsData: { predefinedPrompts: prompts } });
      });

      it('passes on predefined prompts', () => {
        expect(findPredefined().props().prompts).toEqual(prompts);
      });

      it('listens to the click event and sends the predefined prompt', async () => {
        findPredefined().vm.$emit('click', prompts[0]);

        await nextTick();

        expect(wrapper.emitted('send-chat-prompt')).toEqual([[prompts[0]]]);
      });
    });
  });
});
